🔑
AutoKey Pro GCC
مرجع فني لمفاتيح السيارات الخليجية
🔍 بحث السيارات
🔧 دليل Lishi
🔢 حاسبة PIN
✍️ أضف سيارة
🏪 ورشتي
تسجيل الدخول
حساب جديد
إغلاق
وضع عدم الاتصال — تُعرض بيانات محفوظة على جهازك
ابحث عن السيارة
بحث
يتحمّل اختلاف التهجئة: «الانترا» تجد «إلنترا»، و«كامرى» تجد «كامري».
الماركة
جارٍ التحميل...
الموديل
اختر الموديل
سنة الصنع
اختر السنة
نوع المفتاح
كل الأنواع
بصمة (Smart Key)
مفتاح بريموت
مفتاح عادي
السوق
كل الأسواق
خليجي
أمريكي
بحث
حاسبة Pin Code (هيونداي / كيا)
حساب PIN
للموديلات حتى 2006 تقريباً (نظام SMARTRA-2): يُحسب محلياً في المتصفح فوراً بدون إنترنت ولا اشتراك.
للموديلات 2007 فما فوق (SMARTRA-3): يحتاج مصدر بيانات حقيقي (راجع PIN_API_ENDPOINT في الكود) — لا يوجد حساب رياضي مضمون لهذا الجيل.
دليل أدوات Lishi (مرجع سريع)
ابحث برمز الأداة أو اسم السيارة
الدعم والتواصل
لأي استفسار عن حسابك، أو ملاحظة على بيانات سيارة، أو طلب اعتماد —
تواصل معنا مباشرة.
💬 واتساب
✉️ البريد الإلكتروني
حالة حسابك: —
البيانات مرجعية للاستئناس — يُنصح بمطابقتها مع مصدر رسمي قبل الاعتماد عليها في العمل الفعلي.
// ============================================================
// إعدادات الاتصال بـ Supabase
// ============================================================
const SUPABASE_URL = "https://lvikmzphfunvkmpezmin.supabase.co";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx2aWttenBoZnVudmttcGV6bWluIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODgxNDk0NzksImV4cCI6MjEwMzcyNTQ3OX0.O5yUZVOP2SzYBAjQ1URUd89AQXpAzGYuIhu9QtzfhPo";
const TABLE_NAME = "cars_key_data";
const { createClient } = supabase;
// ============================================================
// تعطيل القفل المشترك بين النوافذ
// ============================================================
// مكتبة Supabase تستخدم navigator.locks لتنسيق تجديد التوكن بين
// التبويبات. إذا احتُجز القفل في نافذة أخرى (أو تعطّلت نافذة وهي
// تحمله) فإن getSession تنتظر بلا نهاية — لا تنجح ولا تفشل، فتبقى
// الشاشة معلّقة. نمرّر قفلاً بديلاً ينفّذ مباشرة بلا انتظار.
// المقابل: قد يتجدد التوكن مرتين عند فتح نوافذ متعددة، وهو أمر
// غير ضار هنا إطلاقاً.
const noWaitLock = async (_name, _timeout, fn) => await fn();
// حارس زمني لأي عملية قد تتعلّق
function withTimeout(promise, ms, label){
return Promise.race([
promise,
new Promise((_, rej) => setTimeout(() => rej(new Error('انتهت المهلة: ' + label)), ms))
]);
}
const db = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: { lock: noWaitLock, persistSession: true, autoRefreshToken: true }
});
// ============================================================
// إعدادات حاسبة Pin Code للموديلات الحديثة (2007+, SMARTRA-3)
// حالياً بدون خدمة فعلية — عبّئها لاحقاً إذا اشتركت بمزود حقيقي
// (لا يوجد API عام موثوق مجاني لهذا الجيل، عادة تحتاج دونجل/سوفتوير
// مثل UHS Hardware DiagCode أو خدمة ويب مدفوعة موثوقة)
// ============================================================
const PIN_API_ENDPOINT = "https://example-pin-service.com/api/v1/hyundai-kia";
const PIN_API_KEY = "YOUR_API_KEY_HERE";
const PROG_LABELS = { manual:"يدوية", obd:"جهاز OBD مطلوب" };
// ============================================================
// بيانات الدعم — عدّلها من هنا فقط
// ============================================================
const SUPPORT_WHATSAPP = '966555917073'; // بصيغة دولية بلا +
const SUPPORT_EMAIL = 'nasemm56@hotmail.com';
// حالة وصول المستخدم: guest | pending | suspended | verified
let ACCESS = 'guest';
const LOCKED = '🔒 للفنيين الموثّقين ';
// ============================================================
// تهريب المخرجات — الدفاع الأساسي ضد XSS
// كل قيمة قادمة من قاعدة البيانات (أو من ملف CSV مستورد) تمر من هنا
// قبل إدراجها في الصفحة. بدونها، خانة ملاحظات تحتوي وسماً مثل
// تنفّذ كوداً في متصفح كل فني يفتح البطاقة.
// ============================================================
function esc(v){
if(v === null || v === undefined) return '';
return String(v)
.replace(/&/g,'&')
.replace(//g,'>')
.replace(/"/g,'"')
.replace(/'/g,''');
}
// روابط الصور: نرفض أي مخطط غير http/https (يمنع javascript: و data:)
function safeUrl(v){
const u = String(v || '').trim();
return /^https?:\/\//i.test(u) ? u : '';
}
let CAR_DATA = [];
const selMake = document.getElementById('sel-make');
const selModel = document.getElementById('sel-model');
const selYear = document.getElementById('sel-year');
const selKeyType = document.getElementById('sel-keytype');
const selMarket = document.getElementById('sel-market');
const KEYTYPE_LABELS = { smart:"بصمة", remote:"مفتاح بريموت", blade:"مفتاح عادي" };
const btnSearch = document.getElementById('btn-search');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
function uniq(arr){ return [...new Set(arr)]; }
// ---------- تعبئة القوائم عبر دوال آمنة (بدون سحب الجدول) ----------
// لم نعد نستخدم select * — الجدول مغلق الآن. كل قائمة تُجلب عند الحاجة.
// تسميات حقول البطاقة تتبع اللغة المختارة
function fieldLabels(){
return {
transponder: t('f_chip'), blade: t('f_blade'), freq: t('f_freq'),
prog: t('f_prog'), eeprom: t('f_eeprom'), fcc: t('f_fcc'),
oem: t('f_oem'), special: t('f_special'), steps: t('f_steps'), warn: t('f_warn')
};
}
async function loadData(){
// بلا شبكة: نبني القوائم من النسخة المحفوظة على الجهاز
if(isOffline()){
if(fillFromOffline()){ statusEl.textContent = ""; return; }
statusEl.textContent = "لا يوجد اتصال ولا بيانات محفوظة على هذا الجهاز.";
return;
}
statusEl.textContent = "جارٍ التحميل...";
const { data, error } = await db.rpc('list_makes');
if(error){
// فشل الشبكة رغم أن المتصفح يظنّ نفسه متصلاً
if(fillFromOffline()){
statusEl.textContent = "تعذّر الاتصال — تُعرض بيانات محفوظة على جهازك.";
return;
}
statusEl.textContent = "تعذّر الاتصال بقاعدة البيانات: " + error.message;
console.error(error);
return;
}
selMake.innerHTML = '' + t('sel_make') + ' ';
(data || []).forEach(r => {
const opt = document.createElement('option');
opt.value = r.value; opt.textContent = r.label; // قيمة موحّدة، عنوان كامل
selMake.appendChild(opt);
});
statusEl.textContent = "";
selMake.disabled = false;
}
async function populateModels(){
selModel.innerHTML = '' + t('sel_model') + ' ';
selYear.innerHTML = '' + t('sel_year') + ' ';
selYear.disabled = true;
if(!selMake.value){ selModel.disabled = true; updateSearchState(); return; }
selModel.disabled = true;
if(isOffline() && OFFLINE_CARS){
const gm = new Map();
OFFLINE_CARS.filter(r => arBase(r.make) === selMake.value).forEach(r => {
const b = arBase(r.model);
if(!b) return;
if(!gm.has(b) || r.model.length > gm.get(b).length) gm.set(b, r.model);
});
[...gm.entries()].sort((a,b) => a[1].localeCompare(b[1],'ar')).forEach(([val,lab]) => {
const o = document.createElement('option');
o.value = val; o.textContent = lab;
selModel.appendChild(o);
});
selModel.disabled = false; updateSearchState(); return;
}
const { data, error } = await db.rpc('list_models', { p_make: selMake.value });
if(error){ statusEl.textContent = "تعذّر جلب الموديلات: " + error.message; return; }
(data || []).forEach(r => {
const opt = document.createElement('option');
opt.value = r.value; opt.textContent = r.label;
selModel.appendChild(opt);
});
selModel.disabled = false;
updateSearchState();
}
async function populateYears(){
selYear.innerHTML = '' + t('sel_year') + ' ';
if(!selModel.value){ selYear.disabled = true; updateSearchState(); return; }
selYear.disabled = true;
if(isOffline() && OFFLINE_CARS){
const ys = new Set();
OFFLINE_CARS.filter(r => arBase(r.make) === selMake.value && arBase(r.model) === selModel.value)
.forEach(r => { for(let y = r.year_from; y <= r.year_to; y++) ys.add(y); });
[...ys].sort((a,b) => a-b).forEach(y => {
const o = document.createElement('option');
o.value = y; o.textContent = y;
selYear.appendChild(o);
});
selYear.disabled = false; updateSearchState(); return;
}
const { data, error } = await db.rpc('list_years', { p_make: selMake.value, p_model: selModel.value });
if(error){ statusEl.textContent = "تعذّر جلب السنوات: " + error.message; return; }
(data || []).forEach(r => {
const opt = document.createElement('option');
opt.value = r.year; opt.textContent = r.year;
selYear.appendChild(opt);
});
selYear.disabled = false;
updateSearchState();
}
function updateSearchState(){
btnSearch.disabled = !(selMake.value && selModel.value && selYear.value);
}
selMake.addEventListener('change', populateModels);
selModel.addEventListener('change', populateYears);
selYear.addEventListener('change', updateSearchState);
selMarket.addEventListener('change', updateSearchState);
selKeyType.addEventListener('change', updateSearchState);
btnSearch.addEventListener('click', async () => {
const make = selMake.value;
const model = selModel.value;
const year = parseInt(selYear.value, 10);
const market = selMarket.value;
const keyType = selKeyType.value;
// بلا شبكة: نبحث في النسخة المحفوظة
if(isOffline()){
const local = searchOffline(make, model, year, market, keyType);
if(local === null){
statusEl.textContent = 'لا يوجد اتصال ولا بيانات محفوظة على هذا الجهاز.';
return;
}
renderResults(local.map(mapRow), {make, model, year});
return;
}
btnSearch.disabled = true;
statusEl.textContent = 'جارٍ البحث...';
const { data, error } = await db.rpc('search_cars', {
p_make: make,
p_model: model,
p_year: year,
p_key_type: keyType || null,
p_market: market || null
});
btnSearch.disabled = false;
if(error){
resultsEl.innerHTML = '';
// رسالة تجاوز الحد تأتي من قاعدة البيانات نفسها
statusEl.textContent = /الحد المسموح/.test(error.message)
? '⏳ ' + error.message
: 'تعذّر البحث: ' + error.message;
return;
}
const matches = (data || []).map(mapRow);
renderResults(matches, {make, model, year});
});
function mapRow(row){
return {
id: row.id,
make: row.make,
model: row.model,
yearFrom: row.year_from,
yearTo: row.year_to,
market: row.market,
keyType: row.key_type || '',
transponderType: row.transponder_type,
keyBlade: row.key_blade,
remoteFrequency: row.remote_frequency,
programming: { type: row.programming_type, details: row.programming_details },
eepromLocation: row.eeprom_location,
fccId: row.fcc_id || '',
boardImageUrl: row.board_image_url || '',
oemPartNumber: row.oem_part_number || '',
specialRequirements: row.special_requirements || '',
executionSteps: row.execution_steps || '',
warningsNotes: row.warnings_notes || '',
// صفوف المزامنة لا تحمل هذا الحقل لأنها لا تُمنح إلا للفني الموثّق
isFullAccess: row.is_full_access === undefined ? true : row.is_full_access === true
};
}
// نص الدعوة يتغيّر حسب حالة المستخدم — «سجّل الدخول» بلا معنى لمن هو داخل
function lockedCta(){
if(ACCESS === 'pending') return 'حسابك قيد المراجعة. تواصل مع الدعم ';
if(ACCESS === 'suspended') return 'حسابك موقوف. راسل الدعم ';
return 'سجّل الدخول ';
}
function renderResults(matches, ctx){
resultsEl.innerHTML = '';
if(matches.length === 0){
statusEl.textContent = '';
resultsEl.innerHTML = `🧰
لا توجد بيانات لهذا الموديل/السنة في قاعدة البيانات حالياً.
`;
return;
}
statusEl.textContent = `تم العثور على ${matches.length} نتيجة لـ ${ctx.make} ${ctx.model} ${ctx.year}`.replace(/\s+/g,' ').trim();
matches.forEach(c => {
const marketLabel = c.market === 'gcc' ? 'خليجي' : 'أمريكي';
const keyTypeLabel = KEYTYPE_LABELS[c.keyType] || '';
const progClass = c.programming.type === 'obd' ? 'obd' : 'manual';
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
${esc(c.make)} ${esc(c.model)}
${esc(c.yearFrom)} – ${esc(c.yearTo)}
${marketLabel}
${keyTypeLabel ? `${keyTypeLabel} ` : ''}
نوع الشريحة ${esc(c.transponderType) || '—'}
سلاح المفتاح / لِيشي ${c.isFullAccess ? (esc(c.keyBlade) || '—') : LOCKED}
تردد الريموت ${esc(c.remoteFrequency) || '—'}
طريقة البرمجة ${PROG_LABELS[c.programming.type] || c.programming.type || '—'}${c.isFullAccess ? (c.programming.details ? ' — ' + esc(c.programming.details) : '') : ' — ' + LOCKED}
موقع EEPROM / BCM ${c.isFullAccess ? (esc(c.eepromLocation) || '—') : LOCKED}
${c.isFullAccess && c.oemPartNumber ? `
رقم القطعة الأصلي (OEM) ${esc(c.oemPartNumber)}
` : ''}
${c.fccId ? `
` : ''}
${c.isFullAccess && c.id ? `` : ''}
${!c.isFullAccess ? `🔒 أكواد Lishi وخطوات البرمجة ومواقع EEPROM متاحة للفنيين الموثّقين فقط. ${lockedCta()}
` : ''}
${c.fccId ? `دليل الترددات ورموز FCC ID: افتح الرابط أعلاه لتفاصيل الجهاز الرسمية من سجل FCC الأمريكي (تردد دقيق، الشركة المصنّعة، تقرير الاختبار). الريموتات البديلة (Xhorse VVDI / Autel Universal / KEYDIY) المتوافقة عادة تُطابق حسب نفس التردد ونفس نوع الشريحة الظاهرين أعلاه — تأكد من SKU المحدد المطابق مع موردك قبل الشراء لأن FCC ID وحده لا يضمن التوافق الكامل مع كل الطُرز الفرعية.
` : ''}
${c.specialRequirements ? `⚠ متطلبات خاصة: ${esc(c.specialRequirements)}
` : ''}
${c.executionSteps ? `
خطوات التنفيذ كاملة
${esc(c.executionSteps)}
${c.warningsNotes ? `${esc(c.warningsNotes)}
` : ''}
` : (c.warningsNotes ? `${esc(c.warningsNotes)}
` : '')}
${c.isFullAccess && c.id ? `
` : ''}
`;
resultsEl.appendChild(card);
});
// نجلب صور كل النتائج بطلب واحد بعد رسم البطاقات
attachGalleries(matches.filter(m => m.id).map(m => String(m.id)));
}
// ============================================================
// حاسبة VIN → PIN
// ============================================================
// حاسبة PIN محلية للموديلات القديمة (SMARTRA-2، عادة حتى 2006)
// خوارزمية موثّقة كمصدر مفتوح (Dante383/smartra-vin-to-pin على GitHub)
// تعمل بالكامل داخل المتصفح — بدون إنترنت ولا اشتراك.
// تنويه: هذا نظام قديم بسيط (غير مبني على تشفير قوي)، ولذلك يمكن حسابه
// رياضياً بخلاف الأنظمة الأحدث (SMARTRA-3 فما فوق) التي تحتاج مصدر بيانات
// حقيقي من المصنّع أو مزوّد مرخّص.
// ============================================================
function smartraGetMsb(value){ return (value >>> 16) & 0xFFFF; }
function smartraGetLsb(value){ return value & 0xFFFF; }
function calculateSmartra2Pin(last6DigitsOfVin){
let output = last6DigitsOfVin >>> 0;
for(let index = 0; index <= 0x27; index++){
if((smartraGetMsb(output) & 0x8000) === 0){
output = (output << 1) >>> 0;
}else{
output = (output << 1) >>> 0;
const msb = (smartraGetMsb(output) ^ 0x7798) & 0xFFFF;
const lsb = (smartraGetLsb(output) ^ 0x2990) & 0xFFFF;
output = ((msb << 16) | lsb) >>> 0;
}
}
return output % 1000000;
}
// تخمين سنة الموديل تقريبياً من الخانة العاشرة في VIN (معيار عالمي عام،
// وليس معلومة خاصة بالمناعة/التشفير) — يُستخدم فقط لتحديد أي نظام SMARTRA
// نفترضه، وليس مصدراً دقيقاً 100% بسبب دورة الـ30 سنة المتكررة في المعيار.
const VIN_YEAR_CODES = {
'X':1999,'Y':2000,'1':2001,'2':2002,'3':2003,'4':2004,'5':2005,'6':2006,
'7':2007,'8':2008,'9':2009,'A':2010,'B':2011,'C':2012,'D':2013,'E':2014,
'F':2015,'G':2016,'H':2017,'J':2018,'K':2019,'L':2020,'M':2021,'N':2022,
'P':2023,'R':2024,'S':2025,'T':2026
};
function guessModelYearFromVIN(vin){
const code = vin.charAt(9);
return VIN_YEAR_CODES[code] || null;
}
// ============================================================
// استدعاء API خارجي للموديلات الحديثة (2007+) — فعّله عند الاشتراك
// ============================================================
async function calculatePinFromVIN(vin){
const response = await fetch(PIN_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + PIN_API_KEY
},
body: JSON.stringify({ vin: vin })
});
if(!response.ok){
throw new Error("فشل استدعاء خدمة حساب PIN (HTTP " + response.status + ")");
}
return await response.json(); // المتوقع: { pin: "1234", ... }
}
document.getElementById('btn-pin').addEventListener('click', async () => {
const vinInput = document.getElementById('vin-input');
const resultBox = document.getElementById('pin-result');
const vin = vinInput.value.trim().toUpperCase();
if(vin.length !== 17){
resultBox.className = 'pin-result show';
resultBox.textContent = 'رقم الهيكل يجب أن يكون 17 خانة بالضبط.';
return;
}
const guessedYear = guessModelYearFromVIN(vin);
const last6Str = vin.slice(-6);
const last6Num = Number(last6Str);
const last6IsNumeric = /^[0-9]{6}$/.test(last6Str);
resultBox.className = 'pin-result show';
// الحالة 1: موديل قديم على الأغلب (قبل 2007) — حساب محلي فوري
if(guessedYear && guessedYear < 2007){
if(!last6IsNumeric){
resultBox.textContent = 'آخر 6 خانات من VIN تحتوي حروفاً — هذا غير معتاد لهذا النظام والحساب المحلي قد يكون غير دقيق. تحقق من رقم الهيكل.';
return;
}
const pin = calculateSmartra2Pin(last6Num);
const pinStr = String(pin).padStart(6, '0');
resultBox.innerHTML = `PIN Code (SMARTRA-2, محلي): ${pinStr} موديل متوقع: ${guessedYear} — تحقق دائماً بمصدر رسمي قبل الاعتماد عليه في العمل الفعلي. `;
return;
}
// الحالة 2: موديل حديث على الأغلب (2007+) — يحتاج API حقيقي
if(PIN_API_ENDPOINT.includes('example-pin-service.com') || PIN_API_KEY === 'YOUR_API_KEY_HERE'){
resultBox.innerHTML = `موديل متوقع: ${guessedYear || 'غير معروف'} — هذا الجيل (SMARTRA-3 فما فوق) لا يوجد له حساب رياضي مضمون محلياً. تحتاج تشترك بمصدر حقيقي (دونجل/سوفتوير أو خدمة ويب مرخّصة) وتضع رابط ومفتاح الخدمة في PIN_API_ENDPOINT و PIN_API_KEY داخل الكود.`;
return;
}
resultBox.textContent = 'جارٍ الحساب عبر الخدمة الخارجية...';
try{
const result = await calculatePinFromVIN(vin);
resultBox.textContent = "PIN Code: " + (result.pin || JSON.stringify(result));
}catch(err){
resultBox.textContent = "خطأ: " + err.message;
}
});
// التهيئة تُنفَّذ في نهاية الملف بعد تعريف كل شيء
// ============================================================
// دليل أدوات Lishi (مرجع سريع) — إضافة
// ============================================================
const LISHI_TABLE = "lishi_tools";
let LISHI_DATA = [];
const lishiSearch = document.getElementById('lishi-search');
const lishiList = document.getElementById('lishi-list');
const lishiStatus = document.getElementById('lishi-status');
async function loadLishiTools(){
lishiStatus.textContent = "جارٍ تحميل دليل أدوات Lishi...";
const { data, error } = await db.rpc('list_lishi_tools');
if(error){
lishiStatus.textContent = "تعذّر تحميل دليل Lishi: " + error.message;
console.error(error);
return;
}
LISHI_DATA = data || [];
lishiStatus.textContent = "";
renderLishi("");
}
function renderLishi(query){
const q = query.trim().toLowerCase();
const filtered = q
? LISHI_DATA.filter(t =>
(t.tool_name||'').toLowerCase().includes(q) ||
(t.blade_code||'').toLowerCase().includes(q) ||
(t.supported_cars||'').toLowerCase().includes(q))
: LISHI_DATA;
if(filtered.length === 0){
lishiList.innerHTML = ``;
return;
}
lishiList.innerHTML = filtered.map(t => {
const full = t.is_full_access === true;
return `
${esc(t.tool_name)} (${esc(t.blade_code)})
السيارات المدعومة: ${esc(t.supported_cars)}
القص: ${full ? (esc(t.cut_positions) || '—') : LOCKED}
اتجاه الالتقاط: ${full ? (esc(t.pick_direction) || '—') : LOCKED}
${full && t.notes ? `
ملاحظة: ${esc(t.notes)}
` : ''}
`;
}).join('');
}
lishiSearch.addEventListener('input', () => renderLishi(lishiSearch.value));
// ============================================================
// تسجيل دخول الفنيين (للوصول للتفاصيل الفنية)
// ============================================================
const loginModal = document.getElementById('login-modal');
const accessChip = document.getElementById('access-chip');
const btnOpenLogin = document.getElementById('btn-open-login');
const btnSignout = document.getElementById('btn-signout');
function openLogin(){ loginModal.classList.add('show'); document.getElementById('lg-email').focus(); }
function closeLogin(){ loginModal.classList.remove('show'); }
btnOpenLogin.addEventListener('click', () => { closeDrawer(); openLogin(); });
document.getElementById('btn-close-login').addEventListener('click', closeLogin);
loginModal.addEventListener('click', e => { if(e.target === loginModal) closeLogin(); });
// روابط "سجّل الدخول" داخل بطاقات النتائج
document.addEventListener('click', e => {
const sup = e.target.closest && e.target.closest('.sup-link');
if(sup){
e.preventDefault();
closeDrawer();
switchPane('pane-support');
return;
}
const fix = e.target.closest && e.target.closest('.fix-link');
if(fix){
e.preventDefault();
startCorrection(fix.dataset.id, fix.dataset.label);
return;
}
if(e.target.classList && e.target.classList.contains('login-link')){
e.preventDefault();
closeDrawer();
openLogin();
}
});
function setLgStatus(msg, cls){
const el = document.getElementById('lg-status');
el.className = 'lg-status' + (cls ? ' ' + cls : '');
el.textContent = msg || '';
}
// ============================================================
// بصمة رقم الرخصة
// ============================================================
// الشاشة تَعِد الفني بأن الرقم «يُخزَّن كبصمة مشفّرة فقط». الوفاء
// بهذا الوعد يعني ألا يمرّ الرقم في options.data (تُخزَّن نصاً في
// auth.users). نحفظه مؤقتاً في sessionStorage — ذاكرة التبويب
// وحده، تُمحى بإغلاقه — ونرسله إلى الدالة فور توفّر جلسة.
const PENDING_LICENSE_KEY = 'autokey_pending_license';
async function flushLicenseFingerprint(){
let lic = '';
try { lic = sessionStorage.getItem(PENDING_LICENSE_KEY) || ''; } catch(_){}
if(!lic) return;
try {
const { error } = await db.rpc('set_license_fingerprint', { p_license: lic });
// نمسح المخزَّن في الحالتين: النجاح، أو رفض الدالة للقيمة.
// إبقاؤه بعد رفض صريح يعني محاولة أبدية بلا فائدة.
if(!error || /غير صالح/.test(error.message || '')){
try { sessionStorage.removeItem(PENDING_LICENSE_KEY); } catch(_){}
}
} catch(_){ /* الشبكة — نعيد المحاولة عند الدخول القادم */ }
}
document.getElementById('btn-do-login').addEventListener('click', async () => {
const email = document.getElementById('lg-email').value.trim();
const password = document.getElementById('lg-password').value;
const btn = document.getElementById('btn-do-login');
if(!email || !password){ setLgStatus('أدخل البريد وكلمة المرور.','err'); return; }
btn.disabled = true;
setLgStatus('جارٍ التحقق...','busy');
const { error } = await db.auth.signInWithPassword({ email, password });
btn.disabled = false;
if(error){ setLgStatus('بيانات الدخول غير صحيحة.','err'); return; }
document.getElementById('lg-password').value = '';
setLgStatus('','');
closeLogin();
await flushLicenseFingerprint(); // 🔒 بصمة الرخصة إن كانت معلّقة
await refreshAccess();
});
['lg-email','lg-password'].forEach(id => {
document.getElementById(id).addEventListener('keydown', e => {
if(e.key === 'Enter') document.getElementById('btn-do-login').click();
});
});
// تبديل بين تسجيل الدخول والحساب الجديد
function switchTab(which){
const isLogin = which === 'login';
document.getElementById('pane-login').style.display = isLogin ? '' : 'none';
document.getElementById('pane-signup').style.display = isLogin ? 'none' : '';
document.getElementById('tab-login').classList.toggle('active', isLogin);
document.getElementById('tab-signup').classList.toggle('active', !isLogin);
}
document.getElementById('tab-login').addEventListener('click', () => switchTab('login'));
document.getElementById('tab-signup').addEventListener('click', () => switchTab('signup'));
function setSuStatus(msg, cls){
const el = document.getElementById('su-status');
el.className = 'lg-status' + (cls ? ' ' + cls : '');
el.textContent = msg || '';
}
document.getElementById('btn-do-signup').addEventListener('click', async () => {
const name = document.getElementById('su-name').value.trim();
const workshop = document.getElementById('su-workshop').value.trim();
const city = document.getElementById('su-city').value.trim();
const license = document.getElementById('su-license').value.trim();
const email = document.getElementById('su-email').value.trim();
const password = document.getElementById('su-password').value;
const btn = document.getElementById('btn-do-signup');
if(!name || !email || !password){ setSuStatus('الاسم والبريد وكلمة المرور مطلوبة.','err'); return; }
if(password.length < 6){ setSuStatus('كلمة المرور 6 أحرف على الأقل.','err'); return; }
btn.disabled = true;
setSuStatus('جارٍ إرسال الطلب...','busy');
// 🔒 الرقم في ذاكرة التبويب فقط حتى تتوفّر جلسة نرسله فيها
try {
if(license) sessionStorage.setItem(PENDING_LICENSE_KEY, license);
} catch(_){}
// البيانات الوصفية فقط — الدور والحالة تُحدَّدان في قاعدة البيانات
const { data, error } = await db.auth.signUp({
email, password,
// 🔒 رقم الرخصة لا يمرّ هنا 2026-09-11: كل ما في options.data
// يُخزَّن نصاً صريحاً في auth.users.raw_user_meta_data ويمرّ في
// سجلات المصادقة — وهذا يناقض ما تَعِد به هذه الشاشة حرفياً.
// يُرسل بعد الدخول إلى set_license_fingerprint التي تحسب
// البصمة ولا تحفظ الأصل.
options: { data: {
display_name: name, workshop_name: workshop,
city: city
}}
});
btn.disabled = false;
if(error){
setSuStatus(/already|registered/i.test(error.message)
? 'هذا البريد مسجّل مسبقاً. استخدم تسجيل الدخول.'
: 'تعذّر التسجيل: ' + error.message, 'err');
return;
}
if(data.session) await flushLicenseFingerprint();
['su-name','su-workshop','su-city','su-license','su-email','su-password']
.forEach(id => document.getElementById(id).value = '');
if(data.session){
setSuStatus('✔ تم إنشاء الحساب. طلبك قيد المراجعة.','ok');
await refreshAccess();
}else{
setSuStatus('✔ تم إرسال الطلب. افحص بريدك لتأكيد الحساب، ثم انتظر اعتماد المدير.','ok');
}
});
btnSignout.addEventListener('click', async () => {
closeDrawer();
await db.auth.signOut();
await refreshAccess();
});
// تحديث حالة الوصول وإعادة تحميل ما يعتمد عليها
async function refreshAccess(){
const { data: { session } } = await db.auth.getSession();
if(session){
// نتحقق من الصلاحية الفعلية من قاعدة البيانات، لا من مجرد وجود جلسة
const { data: profile } = await db
.from('profiles')
.select('role, status')
.eq('id', session.user.id)
.maybeSingle();
const verified = profile
&& ['locksmith','moderator','super_admin'].includes(profile.role)
&& profile.status === 'verified';
// نفرّق بين الحالات الأربع: الموقوف ليس كالمعلّق
if(verified) ACCESS = 'verified';
else if(profile && profile.status === 'suspended') ACCESS = 'suspended';
else if(profile) ACCESS = 'pending';
else ACCESS = 'guest';
accessChip.textContent = t('chip_' + ACCESS);
accessChip.classList.toggle('full', ACCESS === 'verified');
accessChip.classList.toggle('bad', ACCESS === 'suspended');
btnSync.style.display = verified ? '' : 'none';
document.getElementById('nav-contrib').style.display = verified ? '' : 'none';
if(verified){ loadMyContribs(); loadWorkshop(); }
else document.getElementById('nav-workshop').style.display = 'none';
// شريط توضيحي لمن ليس موثّقاً — بدل تركه يخمّن سبب الأقفال
showAccessBanner();
btnOpenLogin.style.display = 'none';
btnSignout.style.display = '';
}else{
ACCESS = 'guest';
accessChip.textContent = t('chip_guest');
accessChip.classList.remove('full');
accessChip.classList.remove('bad');
btnSync.style.display = 'none';
showAccessBanner();
document.getElementById('nav-contrib').style.display = 'none';
document.getElementById('nav-workshop').style.display = 'none';
btnOpenLogin.style.display = '';
btnSignout.style.display = 'none';
}
// إعادة تحميل دليل Lishi ومسح النتائج القديمة (تغيّر مستوى الوصول)
await loadLishiTools();
resultsEl.innerHTML = '';
statusEl.textContent = '';
}
// ============================================================
// العمل بدون إنترنت (PWA)
// ============================================================
// نستخدم IndexedDB وليس localStorage لأن الأخير محدود بحوالي 5 ميجابايت
// وسيمتلئ مع نمو قاعدة البيانات لآلاف السجلات.
const DB_NAME = 'autokey-offline';
const STORE = 'kv';
function idb(){
return new Promise((res, rej) => {
const r = indexedDB.open(DB_NAME, 1);
r.onupgradeneeded = () => { r.result.createObjectStore(STORE); };
r.onsuccess = () => res(r.result);
r.onerror = () => rej(r.error);
});
}
async function idbSet(k, v){
const db = await idb();
return new Promise((res, rej) => {
const t = db.transaction(STORE, 'readwrite');
t.objectStore(STORE).put(v, k);
t.oncomplete = () => res(true);
t.onerror = () => rej(t.error);
});
}
async function idbGet(k){
const db = await idb();
return new Promise((res, rej) => {
const t = db.transaction(STORE, 'readonly');
const q = t.objectStore(STORE).get(k);
q.onsuccess = () => res(q.result);
q.onerror = () => rej(q.error);
});
}
let OFFLINE_CARS = null; // نسخة محلية من البيانات
let LAST_SYNC = null;
const offlineBar = document.getElementById('offline-bar');
const offlineText = document.getElementById('offline-text');
const btnSync = document.getElementById('btn-sync');
function isOffline(){ return !navigator.onLine; }
function fmtDate(iso){
if(!iso) return '';
try{
return new Date(iso).toLocaleDateString('ar', { year:'numeric', month:'long', day:'numeric' });
}catch(e){ return iso.slice(0,10); }
}
function updateOfflineBar(){
if(isOffline()){
offlineBar.classList.add('show');
if(OFFLINE_CARS && OFFLINE_CARS.length){
offlineBar.classList.add('synced');
offlineText.textContent =
`📴 بلا اتصال — ${OFFLINE_CARS.length} سجل محفوظ على جهازك (مزامنة ${fmtDate(LAST_SYNC)})`;
}else{
offlineBar.classList.remove('synced');
offlineText.textContent = '📴 بلا اتصال — لا توجد بيانات محفوظة. سجّل الدخول واضغط «تنزيل للعمل بلا نت» عند توفر الشبكة.';
}
}else{
offlineBar.classList.remove('show');
}
}
window.addEventListener('online', () => { updateOfflineBar(); });
window.addEventListener('offline', () => { updateOfflineBar(); });
async function loadOfflineCache(){
try{
const pack = await idbGet('cars');
if(pack && Array.isArray(pack.rows)){
OFFLINE_CARS = pack.rows;
LAST_SYNC = pack.syncedAt;
}
}catch(e){ /* لا يمنع عمل التطبيق */ }
updateOfflineBar();
}
// تنزيل البيانات للاستخدام دون شبكة
btnSync.addEventListener('click', async () => {
closeDrawer();
switchPane('pane-search');
btnSync.disabled = true;
const lbl = btnSync.querySelector('.dl');
const original = lbl.textContent;
lbl.textContent = 'جارٍ التنزيل...';
const { data, error } = await db.rpc('sync_all_cars');
if(error){
lbl.textContent = original;
btnSync.disabled = false;
statusEl.textContent = /الحد المسموح/.test(error.message)
? '⏳ ' + error.message
: 'تعذّر التنزيل: ' + error.message;
return;
}
const rows = data || [];
const syncedAt = new Date().toISOString();
try{
await idbSet('cars', { rows, syncedAt });
OFFLINE_CARS = rows;
LAST_SYNC = syncedAt;
statusEl.textContent = `✔ تم حفظ ${rows.length} سجل على جهازك — يعمل الآن بلا إنترنت`;
}catch(e){
statusEl.textContent = 'تعذّر الحفظ على الجهاز: ' + e.message;
}
lbl.textContent = original;
btnSync.disabled = false;
updateOfflineBar();
});
// البحث محلياً عند انقطاع الشبكة
function searchOffline(make, model, year, market, keyType){
if(!OFFLINE_CARS) return null;
return OFFLINE_CARS.filter(r =>
arBase(r.make) === arBase(make) &&
arBase(r.model) === arBase(model) &&
year >= r.year_from && year <= r.year_to &&
(market === '' || r.market === market) &&
(keyType === '' || r.key_type === keyType)
);
}
// تعبئة القوائم من النسخة المحلية عند انقطاع الشبكة
function fillFromOffline(){
if(!OFFLINE_CARS) return false;
// نجمع على الاسم الأساسي بنفس منطق قاعدة البيانات
const g = new Map();
OFFLINE_CARS.forEach(r => {
const b = arBase(r.make);
if(!b) return;
if(!g.has(b) || r.make.length > g.get(b).length) g.set(b, r.make);
});
selMake.innerHTML = '' + t('sel_make') + ' ';
[...g.entries()].sort((a,b) => a[1].localeCompare(b[1],'ar')).forEach(([val,lab]) => {
const o = document.createElement('option');
o.value = val; o.textContent = lab;
selMake.appendChild(o);
});
selMake.disabled = false;
return true;
}
// ============================================================
// اللغة: عربي / English
// ============================================================
// نطاقها الموقع العام فقط. لوحة التحكم تبقى عربية لأن مستخدمها
// شخص واحد يقرأ العربية، فترجمتها جهد بلا مستفيد.
const LANG = {
ar: {
dir:'rtl', name:'العربية', other:'English',
nav_search:'بحث السيارات', nav_lishi:'دليل Lishi', nav_pin:'حاسبة PIN',
nav_contrib:'أضف سيارة', nav_workshop:'ورشتي', nav_support:'الدعم والتواصل',
nav_sync:'تنزيل للعمل بلا نت', nav_login:'دخول الفنيين',
nav_admin:'لوحة التحكم', nav_logout:'تسجيل الخروج',
h_search:'ابحث عن السيارة',
l_make:'الماركة', l_model:'الموديل', l_year:'سنة الصنع',
l_keytype:'نوع المفتاح', l_market:'السوق',
b_search:'بحث',
ph_quick:'بحث سريع: اسم السيارة، رمز الشريحة، أو FCC ID',
t_search:'بحث السيارات', t_lishi:'دليل Lishi', t_pin:'حاسبة PIN',
t_contrib:'أضف سيارة', t_support:'الدعم والتواصل', t_workshop:'ورشتي',
chip_guest:'زائر', chip_pending:'بانتظار التوثيق',
chip_suspended:'الحساب موقوف', chip_verified:'فني موثّق',
f_chip:'نوع الشريحة', f_blade:'سلاح المفتاح / لِيشي', f_freq:'تردد الريموت',
f_prog:'طريقة البرمجة', f_eeprom:'موقع EEPROM / BCM', f_fcc:'FCC ID',
f_oem:'رقم القطعة الأصلي (OEM)', f_special:'متطلبات خاصة',
f_steps:'خطوات التنفيذ', f_warn:'تحذيرات وملاحظات',
locked:'للفنيين الموثّقين', login_link:'سجّل الدخول',
market_gcc:'خليجي', market_us:'أمريكي',
key_smart:'بصمة', key_remote:'بريموت', key_blade:'عادي',
prog_manual:'يدوية', prog_obd:'جهاز OBD مطلوب',
found:(n,q)=>`تم العثور على ${n} نتيجة لـ ${q}`,
none:'لا توجد بيانات لهذا الموديل/السنة في قاعدة البيانات حالياً.',
sel_make:'اختر الماركة', sel_model:'اختر الموديل', sel_year:'اختر السنة',
all_types:'كل الأنواع', all_markets:'كل الأسواق', loading:'جارٍ التحميل...'
},
en: {
dir:'ltr', name:'English', other:'العربية',
nav_search:'Search cars', nav_lishi:'Lishi guide', nav_pin:'PIN calculator',
nav_contrib:'Add a car', nav_workshop:'My workshop', nav_support:'Support',
nav_sync:'Download for offline use', nav_login:'Technician login',
nav_admin:'Admin panel', nav_logout:'Sign out',
h_search:'Find a vehicle',
l_make:'Make', l_model:'Model', l_year:'Year',
l_keytype:'Key type', l_market:'Market',
b_search:'Search',
ph_quick:'Quick search: car name, transponder, or FCC ID',
t_search:'Search cars', t_lishi:'Lishi guide', t_pin:'PIN calculator',
t_contrib:'Add a car', t_support:'Support', t_workshop:'My workshop',
chip_guest:'Guest', chip_pending:'Pending approval',
chip_suspended:'Account suspended', chip_verified:'Verified technician',
f_chip:'Transponder', f_blade:'Key blade / Lishi', f_freq:'Remote frequency',
f_prog:'Programming method', f_eeprom:'EEPROM / BCM location', f_fcc:'FCC ID',
f_oem:'OEM part number', f_special:'Special requirements',
f_steps:'Procedure', f_warn:'Warnings & notes',
locked:'Verified technicians only', login_link:'Sign in',
market_gcc:'GCC', market_us:'USA',
key_smart:'Smart key', key_remote:'Remote', key_blade:'Blade',
prog_manual:'Manual', prog_obd:'OBD tool required',
found:(n,q)=>`${n} result(s) for ${q}`,
none:'No data for this model/year in the database yet.',
sel_make:'Select make', sel_model:'Select model', sel_year:'Select year',
all_types:'All types', all_markets:'All markets', loading:'Loading...'
}
};
let CUR_LANG = 'ar';
try{ CUR_LANG = localStorage.getItem('autokeyLang') || 'ar'; }catch(e){}
function t(key){
const d = LANG[CUR_LANG] || LANG.ar;
return (key in d) ? d[key] : (LANG.ar[key] !== undefined ? LANG.ar[key] : key);
}
function applyLang(lang){
CUR_LANG = (lang === 'en') ? 'en' : 'ar';
try{ localStorage.setItem('autokeyLang', CUR_LANG); }catch(e){}
const d = LANG[CUR_LANG];
document.documentElement.lang = CUR_LANG;
document.documentElement.dir = d.dir;
document.querySelectorAll('[data-i18n]').forEach(el => {
const v = t(el.dataset.i18n);
if(typeof v === 'string') el.textContent = v;
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const v = t(el.dataset.i18nPh);
if(typeof v === 'string') el.placeholder = v;
});
const lbl = document.getElementById('lang-label');
if(lbl) lbl.textContent = d.other;
// العناصر التي تُبنى بالجافاسكربت تحتاج إعادة رسم
const titles = { 'pane-search':'t_search', 'pane-lishi':'t_lishi',
'pane-pin':'t_pin', 'pane-contrib':'t_contrib',
'pane-support':'t_support', 'pane-workshop':'t_workshop' };
const active = document.querySelector('.tabpane[style*="block"], .tabpane:not([style])');
if(active && titles[active.id]) document.getElementById('page-title').textContent = t(titles[active.id]);
refreshAccessChip();
}
function refreshAccessChip(){
const chip = document.getElementById('access-chip');
if(!chip) return;
chip.textContent = t('chip_' + (ACCESS || 'guest'));
}
document.getElementById('btn-lang').addEventListener('click', () => {
applyLang(CUR_LANG === 'ar' ? 'en' : 'ar');
closeDrawer();
});
// ============================================================
// القائمة الجانبية
// ============================================================
const PANE_TITLES = {
'pane-search':'بحث السيارات',
'pane-lishi':'دليل Lishi',
'pane-pin':'حاسبة PIN',
'pane-contrib':'أضف سيارة',
'pane-support':'الدعم والتواصل',
'pane-workshop':'ورشتي'
};
const drawerEl = document.getElementById('drawer');
const drawerBack = document.getElementById('drawer-back');
function openDrawer(){ drawerEl.classList.add('open'); drawerBack.classList.add('show'); }
function closeDrawer(){ drawerEl.classList.remove('open'); drawerBack.classList.remove('show'); }
document.getElementById('btn-menu').addEventListener('click', openDrawer);
drawerBack.addEventListener('click', closeDrawer);
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeDrawer(); });
function switchPane(id){
document.querySelectorAll('.tabpane').forEach(el => {
el.style.display = (el.id === id) ? 'block' : 'none';
});
document.querySelectorAll('.dnav[data-pane]').forEach(b => {
b.classList.toggle('active', b.dataset.pane === id);
});
const K = { 'pane-search':'t_search','pane-lishi':'t_lishi','pane-pin':'t_pin',
'pane-contrib':'t_contrib','pane-support':'t_support','pane-workshop':'t_workshop' };
document.getElementById('page-title').textContent = K[id] ? t(K[id]) : (PANE_TITLES[id] || '');
window.scrollTo({ top:0 });
}
drawerEl.addEventListener('click', e => {
const b = e.target.closest('.dnav[data-pane]');
if(b){ switchPane(b.dataset.pane); closeDrawer(); }
});
// ---------- البحث السريع ----------
const quickQ = document.getElementById('quick-q');
const btnQuick = document.getElementById('btn-quick');
// تطبيع مطابق لدالة قاعدة البيانات، ليعمل البحث بلا إنترنت بنفس السلوك
const AR_FROM = 'أإآٱىةؤئ٠١٢٣٤٥٦٧٨٩کیچپگژًٌٍَُِّْٰـ';
const AR_TO = 'اااايهوي0123456789كيجبكز';
function arNormalize(v){
let out = '';
for(const ch of String(v || '')){
const i = AR_FROM.indexOf(ch);
if(i === -1) out += ch;
else if(i < AR_TO.length) out += AR_TO[i];
// ما بعد ذلك تشكيل وتطويل — يُحذف
}
return out.replace(/\s+/g, ' ').trim().toLowerCase();
}
// مطابق لدالة ar_base في قاعدة البيانات
function arBase(v){
return arNormalize(v)
.replace(/\([^)]*\)/g, ' ')
.replace(/\s+/g, ' ')
.replace(/[\s/\-]+$/, '')
.trim();
}
async function runQuickSearch(){
const q = quickQ.value.trim();
if(q.length < 2){ statusEl.textContent = 'اكتب حرفين على الأقل.'; return; }
// بلا شبكة: نبحث في النسخة المحفوظة بنفس منطق التطبيع
if(isOffline()){
if(!OFFLINE_CARS){ statusEl.textContent = 'لا يوجد اتصال ولا بيانات محفوظة.'; return; }
const nq = arNormalize(q);
const hits = OFFLINE_CARS.filter(r =>
arNormalize(r.make).includes(nq) ||
arNormalize(r.model).includes(nq) ||
arNormalize(r.transponder_type).includes(nq) ||
arNormalize(r.key_blade).includes(nq) ||
arNormalize(r.fcc_id).includes(nq)
);
resultsEl.innerHTML = '';
statusEl.textContent = `تم العثور على ${hits.length} نتيجة لـ "${q}" (بلا اتصال)`;
renderResults(hits.map(mapRow), {make:q, model:'', year:''});
return;
}
btnQuick.disabled = true;
statusEl.textContent = 'جارٍ البحث...';
const { data, error } = await db.rpc('search_text', { p_query: q });
btnQuick.disabled = false;
if(error){
statusEl.textContent = /الحد المسموح/.test(error.message)
? '⏳ ' + error.message
: 'تعذّر البحث: ' + error.message;
return;
}
statusEl.textContent = `تم العثور على ${(data||[]).length} نتيجة لـ "${q}"`;
renderResults((data || []).map(mapRow), {make:q, model:'', year:''});
}
btnQuick.addEventListener('click', runQuickSearch);
quickQ.addEventListener('keydown', e => { if(e.key === 'Enter') runQuickSearch(); });
// ============================================================
// ورشتي
// ============================================================
let MY_WS = null;
async function loadWorkshop(){
const nav = document.getElementById('nav-workshop');
const { data, error } = await db.rpc('my_workshop');
const row = (!error && data && data.length) ? data[0] : null;
MY_WS = row;
if(!row){ nav.style.display = 'none'; return; }
nav.style.display = '';
document.getElementById('ws-name').textContent = row.name || 'ورشتي';
const isOwner = row.my_role === 'owner';
const free = Math.max(0, row.seat_limit - row.seats_used);
document.getElementById('ws-info').innerHTML =
(row.city ? 'المدينة: ' + esc(row.city) + ' ' : '') +
'دورك: ' + (isOwner ? 'صاحب الورشة' : 'فني') + ' ' +
'المقاعد: ' + row.seats_used + ' من ' + row.seat_limit + ' ' +
(isOwner ? ' — متبقٍ ' + free : '') + ' ' +
(row.status === 'suspended'
? '⛔ الورشة موقوفة — الوصول معطّل لكل فنييها. '
: '');
document.getElementById('ws-owner-box').style.display =
(isOwner && row.status === 'active') ? 'block' : 'none';
loadWsMembers();
}
async function loadWsMembers(){
const el = document.getElementById('ws-members');
const { data, error } = await db.rpc('workshop_members');
if(error){ el.textContent = 'تعذّر التحميل.'; return; }
const rows = data || [];
const isOwner = MY_WS && MY_WS.my_role === 'owner';
el.innerHTML = rows.map(r => `
${esc(r.display_name) || esc(r.email)}
${r.workshop_role === 'owner' ? 'صاحب الورشة ' : ''}
${esc(r.email)} · ${r.status === 'verified' ? 'مفعّل' : esc(r.status)}
${(isOwner && r.workshop_role === 'member')
? `
إزالة `
: ''}
`).join('') || 'لا يوجد فنيون بعد.
';
}
document.getElementById('btn-ws-add').addEventListener('click', async () => {
const email = document.getElementById('ws-email').value.trim();
const el = document.getElementById('ws-status');
if(!email){ el.className='lg-status err'; el.textContent='أدخل بريد الفني.'; return; }
el.className='lg-status busy'; el.textContent='جارٍ الإضافة...';
const { data, error } = await db.rpc('workshop_add_member', { p_email: email });
if(error){ el.className='lg-status err'; el.textContent='✕ ' + error.message; return; }
el.className='lg-status ok'; el.textContent='✔ ' + (data || 'تمت الإضافة');
document.getElementById('ws-email').value = '';
loadWorkshop();
});
document.getElementById('ws-members').addEventListener('click', async (e) => {
const b = e.target.closest('.ws-del');
if(!b) return;
if(!confirm('إزالة هذا الفني؟ سيفقد الوصول ويعود «بانتظار التوثيق».')) return;
const { error } = await db.rpc('workshop_remove_member', { p_user_id: b.dataset.uid });
const el = document.getElementById('ws-status');
if(error){ el.className='lg-status err'; el.textContent='✕ ' + error.message; return; }
loadWorkshop();
});
// ============================================================
// معرض صور البطاقات
// ============================================================
const IMG_KIND_AR = {
remote:'الريموت', board:'البوردة', lishi:'موضع القص',
key:'المفتاح', tool:'الجهاز', other:'صورة'
};
// نجلب صور كل النتائج بطلب واحد بدل طلب لكل بطاقة
async function attachGalleries(ids){
if(!ids.length || ACCESS !== 'verified') return;
const { data, error } = await db.rpc('car_images_for', { p_ids: ids });
if(error || !data || !data.length) return;
const byCar = new Map();
data.forEach(r => {
if(!byCar.has(r.car_id)) byCar.set(r.car_id, []);
byCar.get(r.car_id).push(r);
});
byCar.forEach((imgs, carId) => {
const holder = document.querySelector(`[data-gal="${CSS.escape(carId)}"]`);
if(!holder) return;
// 🔒 safeUrl ترفض javascript: و data: — esc وحدها لا تمنعهما،
// لأنها تمنع الخروج من الخانة لا المخطط داخلها.
holder.innerHTML = imgs.map(im => {
const u = safeUrl(im.url);
if(!u) return '';
return `
${esc(IMG_KIND_AR[im.kind] || 'صورة')}
`;
}).join('');
holder.querySelectorAll('img').forEach(g =>
g.addEventListener('error', () => { g.style.opacity = 0.25; }));
});
}
// ============================================================
// حالة الوصول والدعم
// ============================================================
const WA_URL = 'https://wa.me/' + SUPPORT_WHATSAPP;
const MAIL_URL = 'mailto:' + SUPPORT_EMAIL;
document.getElementById('sup-wa').href = WA_URL;
document.getElementById('sup-mail').href = MAIL_URL;
const ACCESS_TEXT = {
guest: { cls:'', msg:'' },
pending: { cls:'info', msg:'حسابك قيد المراجعة. ستُفتح التفاصيل الفنية فور اعتماده. تواصل مع الدعم ' },
suspended: { cls:'warn', msg:'⛔ تم إيقاف حسابك، والتفاصيل الفنية غير متاحة. راسل الدعم لمعرفة السبب ' },
verified: { cls:'', msg:'' }
};
function showAccessBanner(){
const bar = document.getElementById('access-banner');
const st = ACCESS_TEXT[ACCESS] || ACCESS_TEXT.guest;
if(!st.msg){ bar.className = 'access-banner'; bar.innerHTML = ''; return; }
bar.className = 'access-banner show ' + st.cls;
bar.innerHTML = st.msg;
const SUP = {
pending:['بانتظار التوثيق','أرسل لنا اسمك واسم ورشتك لتسريع الاعتماد.'],
suspended:['موقوف','وضّح لنا ما حدث وسنراجع الحساب.'],
verified:['موثّق','حسابك مفعّل بالكامل.'],
guest:['زائر','سجّل حساباً للوصول للتفاصيل الفنية.']
};
const s2 = SUP[ACCESS] || SUP.guest;
document.getElementById('sup-state').textContent = s2[0];
document.getElementById('sup-hint').textContent = s2[1];
}
// ============================================================
// مساهمات الفنيين
// ============================================================
const CONTRIB_FIELDS = {
'c-make':'make', 'c-model':'model', 'c-yfrom':'year_from', 'c-yto':'year_to',
'c-market':'market', 'c-keytype':'key_type', 'c-chip':'transponder_type',
'c-blade':'key_blade', 'c-freq':'remote_frequency', 'c-eeprom':'eeprom_location',
'c-oem':'oem_part_number', 'c-steps':'execution_steps', 'c-warn':'warnings_notes',
'c-prog-type':'programming_type', 'c-prog-details':'programming_details',
'c-fcc':'fcc_id', 'c-special':'special_requirements',
'c-confidence':'confidence', 'c-source':'data_source'
};
// خانة الاختيار تُقرأ منفصلة لأن قيمتها checked وليست value
function contribEepromFlag(){
const el = document.getElementById('c-req-eeprom');
return el && el.checked ? true : null;
}
function setContribStatus(msg, cls){
const el = document.getElementById('contrib-status');
el.className = 'lg-status' + (cls ? ' ' + cls : '');
el.textContent = msg || '';
}
function resetContribForm(){
Object.keys(CONTRIB_FIELDS).forEach(id => {
const el = document.getElementById(id);
if(el) el.value = (id === 'c-market') ? 'gcc' : '';
});
document.getElementById('c-note').value = '';
const cre = document.getElementById('c-req-eeprom');
if(cre) cre.checked = false;
document.getElementById('c-target').value = '';
document.getElementById('c-target-note').style.display = 'none';
CONTRIB_IMAGES = [];
renderContribImages();
document.getElementById('c-img-status').textContent = '';
document.getElementById('contrib-title').textContent = 'أضف سيارة إلى المرجع';
setContribStatus('','');
}
document.getElementById('btn-contrib-reset').addEventListener('click', resetContribForm);
// تصحيح بطاقة قائمة: يملأ النموذج ويحدد السجل المقصود
function startCorrection(id, label){
document.getElementById('c-target').value = id;
const note = document.getElementById('c-target-note');
note.style.display = 'block';
note.innerHTML = '✍️ تصحيح على: ' + esc(label) + ' — املأ الحقول التي تريد تصحيحها فقط، واترك الباقي فارغاً.';
document.getElementById('contrib-title').textContent = 'تصحيح بيانات سيارة';
switchPane('pane-contrib');
}
// ---------- إرفاق صور بالمساهمة ----------
let CONTRIB_IMAGES = [];
function renderContribImages(){
const el = document.getElementById('c-img-list');
el.innerHTML = CONTRIB_IMAGES.length
? ''
: '';
}
document.getElementById('c-img-list').addEventListener('click', e => {
const a = e.target.closest('a[data-rm]');
if(!a) return;
e.preventDefault();
CONTRIB_IMAGES.splice(Number(a.dataset.rm), 1);
renderContribImages();
});
document.getElementById('btn-c-img').addEventListener('click', async () => {
const files = document.getElementById('c-img-file').files;
const st = document.getElementById('c-img-status');
const btn = document.getElementById('btn-c-img');
const kind = document.getElementById('c-img-kind').value;
if(!files.length){ st.className='lg-status err'; st.textContent='اختر صورة أولاً.'; return; }
btn.disabled = true;
let ok = 0, fail = null;
for(const file of files){
st.className='lg-status busy';
st.textContent = `جارٍ الرفع ${ok + 1} من ${files.length}...`;
// مجلد contrib منفصل: الصور لا تُنشر قبل اعتماد المساهمة
const path = `contrib/${Date.now()}_${file.name.replace(/[^a-zA-Z0-9.\-_]/g,'')}`;
try{
const { error: upErr } = await db.storage.from('car-boards').upload(path, file);
if(upErr) throw upErr;
const { data: pub } = db.storage.from('car-boards').getPublicUrl(path);
CONTRIB_IMAGES.push({ url: pub.publicUrl, kind, caption: null });
ok++;
}catch(err){
fail = (err && err.message) ? err.message : String(err);
break;
}
}
btn.disabled = false;
document.getElementById('c-img-file').value = '';
if(fail){ st.className='lg-status err'; st.textContent = `رُفعت ${ok} ثم فشل: ${fail}`; }
else { st.className='lg-status ok'; st.textContent = `✔ أُرفقت ${ok} صورة`; }
renderContribImages();
});
document.getElementById('btn-contrib-send').addEventListener('click', async () => {
const btn = document.getElementById('btn-contrib-send');
const target = document.getElementById('c-target').value;
const kind = target ? 'correction' : 'new';
const payload = {};
Object.entries(CONTRIB_FIELDS).forEach(([id, key]) => {
const v = (document.getElementById(id).value || '').trim();
if(v !== '') payload[key] = v; // الحقول الفارغة لا تُرسل إطلاقاً
});
if(kind === 'new' && (!payload.make || !payload.model)){
setContribStatus('الماركة والموديل مطلوبان لإضافة سيارة جديدة.','err');
return;
}
const eepromFlag = contribEepromFlag();
if(eepromFlag) payload.requires_eeprom = true;
if(payload.programming_type === 'eeprom') payload.requires_eeprom = true;
if(CONTRIB_IMAGES.length) payload.images = CONTRIB_IMAGES;
if(kind === 'correction' && Object.keys(payload).length === 0 && !CONTRIB_IMAGES.length){
setContribStatus('أدخل الحقل الذي تريد تصحيحه على الأقل.','err');
return;
}
btn.disabled = true;
setContribStatus('جارٍ الإرسال...','busy');
const { error } = await db.rpc('submit_contribution', {
p_kind: kind,
p_payload: payload,
p_note: document.getElementById('c-note').value.trim() || null,
p_target_id: target || null
});
btn.disabled = false;
if(error){
setContribStatus(/الحد المسموح/.test(error.message) ? '⏳ ' + error.message : '✕ ' + error.message, 'err');
return;
}
setContribStatus('✔ وصل اقتراحك للمراجعة. شكراً لك.','ok');
resetContribForm();
loadMyContribs();
});
const CSTAT = { pending:'قيد المراجعة', approved:'✔ اعتُمد', rejected:'✕ رُفض' };
async function loadMyContribs(){
const el = document.getElementById('my-contribs');
const { data, error } = await db.rpc('my_contributions');
if(error){ el.textContent = 'تعذّر التحميل.'; return; }
const rows = data || [];
if(!rows.length){ el.textContent = 'لم ترسل أي مساهمة بعد.'; return; }
el.innerHTML = rows.map(r => `
${esc(r.label) || (r.kind === 'correction' ? 'تصحيح' : 'إضافة')}
${r.review_note ? ''+esc(r.review_note)+' ' : ''}
${CSTAT[r.status] || esc(r.status)}
`).join('');
}
// ============================================================
// إشعار تثبيت التطبيق
// ============================================================
// المتصفح يطلق beforeinstallprompt عندما يرى الموقع مؤهلاً للتثبيت.
// نحتجز الحدث ونعرض شريطاً بلغة المستخدم بدل ترك الأمر لقائمة
// المتصفح التي لا يعرفها كثيرون.
let deferredInstall = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredInstall = e;
if(sessionStorage.getItem('installDismissed') === '1') return;
showInstallBar();
});
function showInstallBar(){
if(document.getElementById('install-bar')) return;
const bar = document.createElement('div');
bar.id = 'install-bar';
bar.className = 'install-bar';
bar.innerHTML =
'🔑
' +
'ثبّت AutoKey Pro ' +
'وصول أسرع، ويعمل داخل الكراج بلا إنترنت
' +
'تثبيت ' +
'لاحقاً ';
document.body.appendChild(bar);
document.getElementById('ib-yes').addEventListener('click', async () => {
if(!deferredInstall) return;
deferredInstall.prompt();
try{ await deferredInstall.userChoice; }catch(e){}
deferredInstall = null;
bar.remove();
});
document.getElementById('ib-no').addEventListener('click', () => {
sessionStorage.setItem('installDismissed','1');
bar.remove();
});
}
// بعد التثبيت لا معنى لبقاء الشريط
window.addEventListener('appinstalled', () => {
const b = document.getElementById('install-bar');
if(b) b.remove();
deferredInstall = null;
});
// تسجيل الـ Service Worker — يجعل التطبيق يفتح بلا شبكة
if('serviceWorker' in navigator){
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(reg => {
// نتحقق من وجود تحديث عند كل فتح للتطبيق
reg.update().catch(() => {});
reg.addEventListener('updatefound', () => {
const nw = reg.installing;
if(!nw) return;
nw.addEventListener('statechange', () => {
// نسخة جديدة جاهزة وهناك نسخة قديمة تعمل حالياً
if(nw.state === 'installed' && navigator.serviceWorker.controller){
showUpdateBanner();
}
});
});
}).catch(err => {
console.warn('تعذّر تسجيل Service Worker:', err);
});
});
}
// إشعار تحديث بدل إعادة تحميل تلقائية — الإعادة التلقائية قد تدخل
// في حلقة لا تنتهي، والفني قد يكون في منتصف عملية برمجة.
function showUpdateBanner(){
if(document.getElementById('update-bar')) return;
const bar = document.createElement('div');
bar.id = 'update-bar';
bar.style.cssText =
'position:fixed; bottom:0; left:0; right:0; z-index:60;' +
'background:var(--amber); color:#1a1206; padding:12px 16px;' +
'display:flex; align-items:center; justify-content:space-between; gap:10px;' +
'font-size:0.82rem; font-weight:600;';
bar.innerHTML =
'يتوفر تحديث للتطبيق ' +
'تحديث الآن ';
document.body.appendChild(bar);
document.getElementById('btn-do-update').addEventListener('click', async () => {
try{
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map(r => r.unregister()));
}catch(e){ /* نكمل على أي حال */ }
location.reload();
});
}
// ============================================================
// التهيئة — الترتيب مقصود
// ============================================================
// يجب أن تُنفَّذ بعد تعريف كل المتغيّرات أعلاه. استدعاء loadData
// قبل تعريف OFFLINE_CARS كان يُسقط التحميل بالكامل عند انقطاع الشبكة.
(async () => {
try{ applyLang(CUR_LANG); }catch(e){ console.warn('تعذّر تطبيق اللغة:', e); }
try{
await loadOfflineCache(); // نقرأ النسخة المحفوظة أولاً
}catch(e){ console.warn('تعذّر قراءة النسخة المحلية:', e); }
try{
await refreshAccess(); // يضبط الصلاحية وزر المزامنة ودليل Lishi
}catch(e){ console.warn('تعذّر تحديث حالة الوصول:', e); }
try{
await loadData(); // تعبئة قائمة الماركات
}catch(e){
console.error(e);
statusEl.textContent = 'تعذّر تحميل القوائم: ' + (e && e.message ? e.message : e);
}
})();
// سنة التذييل — تتحدث تلقائياً كل عام
(function(){
const y = document.getElementById('foot-year');
if(y) y.textContent = new Date().getFullYear();
})();